"""
    OK 0. refactor the notifier with event read 
    OK 1. add timer
    OK 2. add writable
    OK 4. add readable
    OK 5. mapping reactor's self.__events into timer only
    OK 6. easy connect impl to nonblock tcp transport
    OK 7. easy threaded wrapper running for reactor
"""

import threading
import selectors
import logging
import time
import heapq

from bcat.error import InvalidArgumentError
from error import InvalidOperationError

from event import Event
from event_creator import EventCreator
from event_queue import EventQueue
from fileno_map import FileNoMap
from notifier import Notifier

log = logging

class Reactor:
    # state of reactor
    STATE_INITED = 0
    STATE_RUNNING = 1
    STATE_EXIT = 2

    #Const of default value
    FLOAT_NS_PER_SECOND = 1000000000.0
    DEFAULT_WAIT_NS = 100*1000000

    
    def __init__(self):
        self.__lock = threading.Lock()
        self.__is_running = Reactor.STATE_INITED

        self.__events = {} # identity -> [event]
        self.__queue = EventQueue()

        self.__selector = selectors.DefaultSelector()
        self.__notifier = Notifier()


        self.__file_events = FileNoMap()

        self.__timeout_events = []
        
        self.__pending = []

    def _register_nolock(self, event):
        identity = event.id()
        if identity not in self.__events: 
            self.__events[identity] = event
            self.__queue.push_back_register(event)
            return True
        return False
    def register(self, event):
        is_registered = False
        with self.__lock:
            if self.__is_running != Reactor.STATE_EXIT:
                is_registered = self._register_nolock(event)
                if not is_registered:
                    log.error(f"register event({event}) failed: exists in loop")
                    raise InvalidArgumentError(f"event:{event} is exists", 1)
            else:
                log.error(f"InvalidOperationError:register event({event}) failed: is_running(STATE_EXIT)")
                raise InvalidOperationError(f"register event:{event} is not allowed, because of is_running(STATE_EXIT)")
        #end of locked 

        if is_registered:
            self.__notifier.signal(b'r')

    def _unregister_nolock(self, identity):
        if identity in self.__events:
            event = self.__events.pop(identity)
            self.__queue.push_back_unregister(event)
            return True
        return False
    def unregister(self, identity):
        is_unregistered = False
        with self.__lock:
            if self.__is_running != Reactor.STATE_EXIT:
                is_unregistered = self._unregister_nolock(identity)
                if not is_unregistered:
                    log.error(f"unregister event_id({identity})failed: not exists in loop")
                    raise InvalidArgumentError(f"identity:{identity} not exists", 1)
            else:
                log.error(f"InvalidOperationError:unregister event id({identity}) failed: is_running(STATE_EXIT)")
                raise InvalidOperationError(f"unregister event_id:{identity} is not allowed, because of is_running(STATE_EXIT)")
        #end of locked 

        if is_unregistered:
            self.__notifier.signal(b'u')

    def _prepare_notify(self):
        log.debug("prepare notifier:{}".format(self.__notifier))
        ev_notify = EventCreator.from_readable(self.__notifier.fno(), self.__notifier, True)
        if self._register_nolock(ev_notify):
            self.__notifier.set_id(ev_notify.id())
        else:
            raise InvalidArgumentError("register the notifier error!")

    def _clean_notify(self):
        log.debug("clean notifier resource,id:[{}]".format(self.__notifier.id()))
        self._unregister_nolock(self.__notifier.id())
        self.__notifier.close()

    # return 0 => if deleted()  or ev.timeout() <= now => skip the wait => delete timer
    # use default_wait_ns if heap is empty() => wait(some span)
    # or use the min_ns => for select(wait)
    def __min_timeout_ns(self, default_wait_ns):
        if len(self.__timeout_events) == 0:
            return default_wait_ns

        min_ev = self.__timeout_events[0]
        if min_ev.is_deleted():
            return 0

        now = time.monotonic_ns()
        return 0 if min_ev.timeout() <= now else min_ev.timeout() - now

    def loop(self):
        self.__lock.acquire()

        self._prepare_notify()

        self.__is_running = Reactor.STATE_RUNNING
        while self.__is_running == Reactor.STATE_RUNNING:
            # start = time.monotonic_ns()
            # remove all delay<=0 timer & unregistered timer
            log.debug("loop round start.....")

            #1. from queue to add R/W/T events
            while not self.__queue.empty():
                (op, ev) = self.__queue.pop_front()
                log.debug(f"pop from queue: {ev}")

                if EventQueue.OperationEvent.Operation.is_register(op):
                    self.__add_event(ev)
                elif EventQueue.OperationEvent.Operation.is_unregister(op):
                    self.__delete_event(ev)
                else:
                    log.error(f"Operation:{op} for Event:{ev} is unknown")
            #end while queue not empty

            log.debug("file_events: count:{}, content:{}".format(len(self.__file_events), self.__file_events))
            log.debug("timeout events: count:{}, content:{}".format(len(self.__timeout_events), self.__timeout_events))
            log.debug("global events: count:{}, content:{}".format(len(self.__events), self.__events))

            #2. selector wait with unlock
            wait_sec = self.__min_timeout_ns(Reactor.DEFAULT_WAIT_NS)/Reactor.FLOAT_NS_PER_SECOND
            log.debug("wait milli-second:{} ms for selector".format(wait_sec*1000)) 
            self.__lock.release()
            events = self.__selector.select(wait_sec)
            self.__lock.acquire()

            
            #3. read write event handle
            for key, mask in events:
                fileno = key.data
                if mask & selectors.EVENT_READ != 0: 
                    #check  fileno & readable_event is  exists in file_events
                    ev = self.__file_events.get_readable_event(fileno)
                    log.debug("read event fired, fd:{}".format(fileno.fileno()))
                    self.__lock.release()
                    ev.call_handler()
                    self.__lock.acquire()
                    if not ev.persistent():
                        self.__pending.append(ev)

                if mask & selectors.EVENT_WRITE != 0:
                    #check  fileno & writable_event is exists in file_events
                    ev = self.__file_events.get_writable_event(fileno)
                    log.debug("write event fired, fd:{}".format(fileno.fileno()))
                    self.__lock.release()
                    ev.call_handler()
                    self.__lock.acquire()
                    if not ev.persistent():
                        self.__pending.append(ev)

            #4. handle pending file-events
            for ev in self.__pending:
                log.debug(f"handle pending ev:{ev}")
                self.__delete_event(ev)
                if not ev.persistent() and ev.id() in self.__events:
                    del self.__events[ev.id()]
            self.__pending.clear()

            #5. timeout event handle: delete the event & call it 
            self.process_timeout()

        #end of while self.__is_running  == Reactor.STATE_RUNNING

        self._clean()

    #end of loop(self)

    def process_timeout(self):
        if len(self.__timeout_events) == 0:
            return 

        now = time.monotonic_ns()
        while len(self.__timeout_events) > 0:
            ev = self.__timeout_events[0]
            assert(ev is not None)

            if ev.is_deleted():
                heapq.heappop(self.__timeout_events)
            else:
                if ev.timeout() > now:
                    break
                else: #timeout <= now
                    ev = heapq.heappop(self.__timeout_events)
                    ev.call_handler()
                    if ev.persistent():
                        self.__add_event(ev)
    #end process_timeout
    

    #add all event
    def __add_event(self, ev):
        #readable event register
        if Event.is_file_events(ev):
            fileno = ev.fileno()
            if self.__file_events.contains_fileno(fileno): #fileno in map
                if self.__file_events.contains_fileno_type(fileno, ev.type()): #fileno & type in map
                    log.debug(f"register an readable event of fileno:{fileno} exists in map")
                else: #fileno in map, but ev.type not in map
                    assert(self.__file_events.count(fileno) == 1)
                    log.debug(f"register(modify) the READ|WRITE event of fileno:{fileno}")
                    self.__selector.modify(fileno, selectors.EVENT_READ | selectors.EVENT_WRITE, fileno)
                    self.__file_events.add(fileno, ev.type(), ev)
            else: #not in file_map
                selectors_events = selectors.EVENT_READ  if Event.is_readable(ev) else selectors.EVENT_WRITE
                log.debug(f"register [{selectors_events}(1-RD|2-WR)] event of fileno:{fileno} into selector")
                self.__selector.register(fileno, selectors_events, fileno)
                self.__file_events.add(fileno, ev.type(), ev)
            
        #timer event register 
        elif Event.is_timer(ev):
            heapq.heappush(self.__timeout_events, ev.set_timeout_with_now(time.monotonic_ns()))
            log.debug(f"register a timeout event:{ev}")
        else:
            log.error(f"unknown event:{ev}")
         

    def __delete_file_event(self, fileno, ev_type):
        if self.__file_events.contains_fileno_type(fileno, ev_type):
            self.__file_events.remove(fileno, ev_type)
            if not self.__file_events.contains_fileno(fileno):
                log.debug(f"unregister fileno:{fileno} from selector")
                self.__selector.unregister(fileno)
            else: # fileno left in file_events after delete ev
                assert(self.__file_events.count(fileno) == 1)
                selectors_events =  selectors.EVENT_READ if Event.Type.Readable == self.__file_events.get_event_type(fileno) else selectors.EVENT_WRITE
                log.debug(f"unregister(modify) [{selectors_events}(1-RD|2-WR] event of fileno:{fileno} to selector")
                self.__selector.modify(fileno, selectors_events, fileno)
            return True
        return False
        
    # delete all events
    def __delete_event(self, ev):
        if Event.is_file_events(ev):
            if not self.__delete_file_event(ev.fileno(), ev.type()):
                log.debug("an readable event of fileno:{} exists in map".format(ev.fileno()))
        elif Event.is_timer(ev):
            ev.delete()
            log.debug(r"unregister a timeout event:{ev}")
        else:
            log.error(f"unknown event:{ev}")

        

    def _clean(self):
        self.__timeout_events.clear()
        self.__file_events.clear()

        self._clean_notify()
        self.__selector.close()

        self.__queue.clear()
        self.__events.clear()
        self.__lock.release()
        

    def loop_exit(self):
        with self.__lock:
            if self.__is_running != Reactor.STATE_EXIT:
                self.__is_running = Reactor.STATE_EXIT
                self.__notifier.signal(b'e') #signal-exit
            else:
                log.error("loop is stoped already when call loop exit")

    def is_running(self):
        with self.__lock:
            return self.__is_running


    def loop_in_thread(self):
        class Runner(threading.Thread):
            def __init__(self, reactor):
                self.__reactor = reactor
                super().__init__()
            def run(self):
                self.__reactor.loop()
        thread = Runner(self)
        thread.start()
        return thread

